(function(jString) {
	var i;
	
	//extend String object with jString
	for(i in jString) {
		String.prototype[i] = (function() {
			var mtd = jString[i];
			return function() {
				var args = [this.toString()], i;
				for(i = 0; i < arguments.length; i++)
					args[args.length] = arguments[i];
				return mtd.apply(this, args);
			};
		})();
	}
	
	//insert jString to global scope
	window["jString"] = jString;
	
})((function() {
	
	/**
	 * Check and adjust boundaries
	 * 
	 * @param int index
	 * @param int def Default index value
	 * @param int len Length
	 * @return int Adjusted index
	 */
	function adjustBounds(index, def, len) {
		if(isNaN(index))
			return def;
		else if(index < 0)
			return (-index > len ? 0 : len + index);
		else if(index > len)
			return len;
		
		return index;
	}
	
	
	/**
	 * Check whether value is empty
	 * 
	 * @param mixed val
	 * @return boolean
	 */
	function isEmpty(val) {
		if(val === undefined || val === null || val === "")
			return true;
		
		return false;
	}
	
	
	/**
	 * Replace substring from start to start + len with string repl
	 * If len is omitted, use length of repl
	 * 
	 * @param string str Source string
	 * @param string repl Replacement string
	 * @param int start Start index
	 * @param int len Replacement length
	 * @return string
	 */
	function replacesub(str, repl, start, len) {
		start = adjustBounds(start, 0, str.length);
		
		if(isNaN(len))
			len = repl.length;
		
		return str.slice(0, start) + repl + str.slice(start + len);
	}
	
	
	/**
	 * Strip characters in set chars from beginning of string
	 * If chars is omitted, use whitespace
	 * 
	 * @param string str Source string
	 * @param string chars Set of characters to strip
	 * @return string
	 */
	function lstrip(str, chars) {
		var i;
		
		if(chars === undefined || chars === null)
			chars = " \r\n\t";
		
		for(i = 0; i < str.length; i++) {
			if(chars.indexOf(str.charAt(i)) === -1)
				break;
		}
		
		if(i) str = str.substring(i);
		
		return str;
	}
	
	
	/**
	 * Strip characters in set chars from end of string
	 * If chars is omitted, use whitespace
	 * 
	 * @param string str Source string
	 * @param string chars Set of characters to strip
	 * @return string
	 */
	function rstrip(str, chars) {
		var i;
		
		if(chars === undefined || chars === null)
			chars = " \r\n\t";
		
		for(i = str.length - 1; i >= 0; i--) {
			if(chars.indexOf(str.charAt(i)) === -1)
				break;
		}
		
		if(++i < str.length)
			str = str.substring(0, i);
		
		return str;
	}
	
	
	/**
	 * Strip characters in set chars from beginning and end of string
	 * If chars is omitted, use whitespace
	 * 
	 * @param string str Source string
	 * @param string chars Set of characters to strip
	 * @return string
	 */
	function strip(str, chars) {
		if(chars === undefined || chars === null)
			chars = " \r\n\t";
		
		return rstrip(lstrip(str, chars), chars);
	}
	
	
	/**
	 * Return string filled with str up to given width
	 * 
	 * @param string str Fill string
	 * @param int width Output width
	 * @param int offset Offset of str to start filling, default 0
	 * @return string
	 */
	function fill(str, width, offset) {
		var ret = "", l = str.length, i;
		
		if(isNaN(width) || l < 1)
			return ret;
		
		//if offset is used, split string and swap places
		if(!isNaN(offset) && offset !== 0) {
			if(offset < 0)
				offset = l + offset;
			str = str.substr(offset) + str.substr(0, offset)
		}
		
		for(i = Math.floor(width / l); i > 0; i--)
			ret += str;
		
		l = width % l;
		if(l) ret += str.substring(0, l);
		
		return ret;
	}
	
	
	/**
	 * Align string centered in given width, using fstr for filling
	 * 
	 * @param string str Source string
	 * @param int width String width
	 * @param string fstr Fill string
	 * @return string
	 */
	function center(str, width, fstr) {
		var l = str.length, pre, post;
		
		if(isNaN(width) || width <= l)
			return str;
		
		if(fstr === undefined || fstr === null || fstr.length < 1)
			fstr = " ";
		
		pre = Math.floor((width - l) / 2);
		post = width - l - pre;
		
		return fill(fstr, pre) + str + fill(fstr, post, (l + pre) % fstr.length);
	}
	
	
	/**
	 * Align string to left in given width, using fstr for filling
	 * 
	 * @param string str Source string
	 * @param int width String width
	 * @param string fstr Fill string
	 * @return string
	 */
	function ljust(str, width, fstr) {
		if(isNaN(width) || width <= str.length)
			return str;
		
		if(fstr === undefined || fstr === null || fstr.length < 1)
			fstr = " ";
		
		return str + fill(fstr, width - str.length, str.length % fstr.length)
	}
	
	
	/**
	 * Align string to right in given width, using fstr for filling
	 * 
	 * @param string str Source string
	 * @param int width String width
	 * @param string fstr Fill string
	 * @return string
	 */
	function rjust(str, width, fstr) {
		if(isNaN(width) || width <= str.length)
			return str;
		
		if(fstr === undefined || fstr === null || fstr.length < 1)
			fstr = " ";
		
		return fill(fstr, width - str.length) + str;
	}
	
	
	/**
	 * Count number of non overlapping sub occurences in str, 
	 * starting from offset start up to offset end
	 * 
	 * @param string str Source string
	 * @param string sub String to search
	 * @param int start Start offset, default 0
	 * @param int end End offset, default str length
	 * @return int
	 */
	function count(str, sub, start, end) {
		var l = sub.length, cnt = 0, i;
		
		if(l < 1)
			return cnt;
		
		start = adjustBounds(start, 0, str.length);
		end = adjustBounds(end, str.length, str.length);
		
		while((i = str.indexOf(sub, start)) !== -1) {
			if(i + l > end)
				break;
			
			cnt++;
			start = i + l;
		}
		
		return cnt;
	}
	
	
	/**
	 * Check whether string is empty
	 * 
	 * @param string str Source string
	 * @return boolean True if empty
	 */
	function empty(str) {
		return str === "";
	}
	
	
	/**
	 * Check whether string is blank, that is, contains only whitespace
	 * 
	 * @param string str Source string
	 * @return boolean True if blank
	 */
	function blank(str) {
		return str.search(/[^\s]/g) === -1;
	}
	
	
	/**
	 * Check whether string from start to end starts with sub
	 * 
	 * @param string str Source string
	 * @param string sub String to test
	 * @param int start Start offset, default 0
	 * @param int end End offset, default str length
	 * @return boolean True if substring starts with sub
	 */
	function startswith(str, sub, start, end) {
		start = adjustBounds(start, 0, str.length);
		end = adjustBounds(end, str.length, str.length);
		
		//if sub is longer than string to search
		if(sub.length > end - start)
			return false;
		
		return (str.indexOf(sub, start) === start);
	}
	
	
	/**
	 * Check whether string from start to end ends with sub
	 * 
	 * @param string str Source string
	 * @param string sub String to test
	 * @param int start Start offset, default 0
	 * @param int end End offset, default str length
	 * @return boolean True if substring ends with sub
	 */
	function endswith(str, sub, start, end) {
		var pos;
		
		start = adjustBounds(start, 0, str.length);
		end = adjustBounds(end, str.length, str.length);
		
		//if sub is longer than string to search
		if(sub.length > end - start)
			return false;
		
		pos = str.lastIndexOf(sub, end);
		
		return (pos !== -1 && pos === end - sub.length);
	}
	
	
	/**
	 * Check whether string from start to end contains sub
	 * 
	 * @param string str Source string
	 * @param string sub String to test
	 * @param int start Start offset, default 0
	 * @param int end End offset, default str length
	 * @return boolean True if substring contains sub
	 */
	function contains(str, sub, start, end) {
		var pos;
		
		start = adjustBounds(start, 0, str.length);
		end = adjustBounds(end, str.length, str.length);
		
		//if sub is longer than string to search
		if(sub.length > end - start)
			return false;
		
		pos = str.indexOf(sub, start);
		
		return (pos !== -1 && pos <= end - sub.length);
	}
	
	
	/**
	 * Encode HTML special characters to their corresponding entities
	 * 
	 * @param string str Source string
	 * @param boolean quotes Whether to encode single and double quotes
	 * @return string
	 */
	function encodehtml(str, quotes) {
		var map = {"&amp;": /&/g, "&lt;": /\</g, "&gt;": /\>/g}, ch;
		for(ch in map)
			str = str.replace(map[ch], ch);
		
		if(quotes) {
			map = {"&quot;": /"/g, "&#39;": /'/g};
			for(ch in map)
				str = str.replace(map[ch], ch);
		}
		
		return str;
	}
	
	
	/**
	 * Join elements of array to string using str as a separator
	 * 
	 * @param string str Separator string
	 * @param array parts Array of elements
	 * @return string
	 */
	function join(str, parts) {
		var ret = "", l = parts.length, i;
		
		if(l < 1)
			return ret;
		
		l--;
		for(i = 0; i < l; i++)
			ret += parts[i] + str;
		
		ret += parts[i];
		
		return ret;
	}
	
	
	/**
	 * Truncate str to given length. If truncation takes place, append suffix to substring
	 * 
	 * @param string str Source string
	 * @param int len Maximum length
	 * @param string suffix String to append to truncated string, default to "..."
	 * @return string
	 */
	function truncate(str, len, suffix) {
		len = adjustBounds(len, str.length, str.length);
		
		if(str.length <= len)
			return str;
		
		if(suffix === undefined || suffix === null)
			suffix = "...";
		
		if(suffix.length > len)
			suffix = suffix.substring(0, len);
		
		return str.substring(0, len - suffix.length) + suffix;
	}
	
	
	/**
	 * Capitalize each word in string
	 * 
	 * @param string str Source string
	 * @return string
	 */
	function ucwords(str) {
		return str.replace(/(?:^(.)|\s(.))/g, function(c) {
			return c.toUpperCase();
		});
	}
	
	
	/**
	 * Capitalize first word in string
	 * 
	 * @param string str Source string
	 * @return string
	 */
	function capitalize(str) {
		return str.replace(/^\s*(.)/g, function(c) {
			return c.toUpperCase();
		});
	}
	
	
	/**
	 * Formatter wrapper, replacement parameters passed as object
	 * 
	 * @param string fmt Formatting string
	 * @param object repl Replacements
	 * @return string Formatted string
	 */
	function vformat(fmt, repl) {
		var f = new Formatter(fmt, repl);
		return f.parse();
	}
	
	
	/**
	 * Formatter wrapper, replacement parameters passed as arguments
	 * 
	 * @param string fmt Formatting string
	 * @param ... mixed Variable amount of replacements
	 * @return string Formatted string
	 */
	function format(fmt) {
		var repl = {}, i;
		for(i = 1; i < arguments.length; i++)
			repl[i - 1] = arguments[i];
		
		return vformat(fmt, repl);
	};
	
	
	/**
	 * Formatter class to handle format and vformat
	 * 
	 * @param string fmt Formatting string
	 * @param object repl object of replacement data
	 */
	function Formatter(fmt, repl) {
		this.fmt = fmt;
		this.repl = repl;
	}
	
	//regexp to parse formatting string
	Formatter.prototype.fs_re = /^(?:(.)?([\<\>=\^]))?([\+\- ])?(#)?(0)?(\d+)?(?:\.(\d+))?([\w%])?$/;
	
	//regexp to test whether value is infinity
	Formatter.prototype.inf_re = /^(\-)?Infinity$/i;
	
	//numeric prefixes
	Formatter.prototype.num_prefixes = {"b": "0b", "o": "0o", "x": "0x", "X": "0x"};
	
	
	/**
	 * Parse formatting string and perform replacements
	 * 
	 * @return string Formatted string
	 */
	Formatter.prototype.parse = function() {
		var c, depth = 0, start = null, i, tok, val;
		
		for(i = 0; i < this.fmt.length; i++) {
			c = this.fmt.charAt(i);
			
			//replacement token start
			if(c === "{") {
				//escaped curly bracket
				if(this.fmt.length > i + 1 && this.fmt.charAt(i + 1) === "{") {
					this.fmt = replacesub(this.fmt, "", i, 1);
					continue;
				}
				
				//if not inside token, mark start
				if(depth === 0)
					start = i;
				
				depth++;
			}
			//replacement token end
			else if(c === "}") {
				//escaped curly bracket
				if(depth == 0 && this.fmt.length > i + 1 && this.fmt.charAt(i + 1) === "}") {
					this.fmt = replacesub(this.fmt, "", i, 1);
					continue;
				}
				
				depth--;
				//if depth goes negative, we have an error, ignore it silently, reset depth and continue
				if(depth < 0) {
					depth = 0;
					continue;
				}
				//closing token at depth 0, apply replacement
				else if(depth == 0) {
					//parse replacement token
					tok = this.fmt.substring(start + 1, i);
					val = this.parseReplacement(tok);
					//if parse succeeded, replace token in format string
					if(val !== null) {
						this.fmt = replacesub(this.fmt, val, start, i - start + 1);
						i = start + val.length - 1;
						continue;
					}
				}
			}
		}
		
		return this.fmt;
	};
	
	/**
	 * Parse replacement token
	 * 
	 * @param string str Token string
	 * @return string Replacement or null if parsing failed
	 */
	Formatter.prototype.parseReplacement = function(str) {
		var start = 0, end, i, tmp, val, fs;
		
		i = str.indexOf(":");
		//no end of field found, so str contains only field name
		if(i === -1)
			i = str.length;
		
		//no field name set
		else if(i === start)
			return null;
		
		tmp = str.substring(start, i);
		val = this.parseFieldName(tmp);
		
		//check that value is formattable
		if(val === null || !val.toString)
			return null;
		
		//if format_spec
		if(i < str.length && str.charAt(i) === ":") {
			fs = str.substring(i + 1);
			i = 0;
			
			//replace nested replacements
			start = i;
			while((start = fs.indexOf("{", start)) !== -1) {
				end = fs.indexOf("}", start);
				if(end === -1)
					return null;
				
				tmp = this.parseFieldName(fs.substring(start + 1, end));
				if(tmp === null)
					return null;
				
				fs = replacesub(fs, tmp, start, end - start + 1);
				start++;
			}
			
			val = this.processFormatting(val, fs);
		//just cast to string
		} else {
			val = (val.toString ? val.toString() : null);
		}
		
		return val;
	};
	
	
	/**
	 * Process value formatting by given formatting string
	 * 
	 * @param mixed val Value
	 * @param string fs Formatting string
	 * @return string Formatted value
	 */
	Formatter.prototype.processFormatting = function(val, fs) {
		var tmp, num, exp, pre, i,
			fill, align, sign, prefix, zeropad, width, precision, type;
		
		tmp = fs.match(this.fs_re);
		if(tmp === null)
			return null;
		
		fill = (isEmpty(tmp[1]) ? " " : tmp[1]);
		align = (isEmpty(tmp[2]) ? "<" : tmp[2]);
		sign = (isEmpty(tmp[3]) ? "-" : tmp[3]);
		prefix = (tmp[4] === "#");
		zeropad = (tmp[5] === "0");
		width = tmp[6];
		precision = (isEmpty(tmp[7]) ? 6 : new Number(tmp[7]));
		type = tmp[8];
		
		//if zeropad is used (note that fill has higher precedence if it's present)
		if(zeropad && isEmpty(tmp[1])) {
			fill = "0";
			align = "=";
		}
		
		//if type is set
		if(!isEmpty(type)) {
			//convert to Number
			num = new Number(val);
			val = null;
			
			//prepare sign character
			if(num >= 0 || isNaN(num)) {
				if(sign === "+" || sign === " ");
				else sign = "";
			}
			else sign = "";
			
			// not a number
			if(isNaN(num)) {
				val = sign + "nan";
				//if uppercase type
				if("FEG".indexOf(type) > -1)
					val = val.toUpperCase();
				//if percent type
				if(type === "%")
					val += "%";
			}
			// infinity
			else if((tmp = this.inf_re.exec(num)) !== null) {
				if(tmp[1] === "-")
					sign = "-";
				val = sign + "inf";
				//if uppercase type
				if("FEG".indexOf(type) > -1)
					val = val.toUpperCase();
				//if percent type
				if(type === "%")
					val += "%";
			}
			//base 10
			else if(type === "d") {
				val = sign + num.toFixed(0);
			}
			//base 2, 8 and 16
			else if((tmp = ({"b": 2, "o": 8, "x": 16, "X": 16})[type]) !== undefined) {
				//convert to integer with no decimals
				val = (new Number(num.toFixed(0))).toString(tmp);
				if(prefix) {
					//if value has minus sign, strip it before prepending prefix
					if((tmp = val.charAt(0)) === '-')
						val = val.substring(1);
					else
						tmp = "";
					
					val = tmp + this.num_prefixes[type] + val;
				}
				
				//uppercase hexadecimal
				if(type === "X")
					val = val.toUpperCase();
				
				val = sign + val;
			}
			//character
			else if(type === "c") {
				if(num >= 0 && num <= 0xffff) {
					val = String.fromCharCode(num.toFixed(0));
				}
			}
			//fixed point
			else if(type === "f" || type === "F") {
				val = sign + num.toFixed(precision);
			}
			//exponent
			else if(type === "e" || type === "E") {
				val = sign + num.toExponential(precision);
				
				//zeropad exponent part to minimum length of 2
				val = val.replace(/(e[\+\-])(\d+)/, function(m, a, exp) {
					return a + rjust(exp, 2, "0");
				});
				
				if(type === "E")
					val = val.toUpperCase();
			}
			//general number
			else if(type === "g" || type === "G") {
				if(precision < 1)
					precision = 1;
				
				//get exponent part should number be formatted with precision - 1
				tmp = num.toExponential(precision - 1);
				exp = parseInt(tmp.match(/e([\+\-]\d+)$/)[1]);
				//if -4 <= exp < precision, format as fixedpoint
				if(-4 <= exp && exp < precision) {
					tmp = precision - 1 - exp;
					val = num.toFixed(tmp > 0 ? tmp : 0);
				//else format as exponential
				} else {
					val = tmp;
				}
				
				//strip insignificant trailing zeroes and pad exponent part
				val = val.replace(/^(\-?\d+)(?:\.(\d+))?(?:(e[\+\-])(\d+))?$/, 
					function(m, a, decimals, b, exp) {
					var ret = "" + a;
					//if number has decimals
					if(!isEmpty(decimals)) {
						decimals = rstrip(decimals, "0");
						if(decimals !== "")
							ret += "." + decimals;
					}
					//if number has exponent part
					if(!isEmpty(b))
						ret += b + rjust(exp, 2, "0");
					
					return ret;
				});
				val = sign + val;
				
				if(type === "G")
					val = val.toUpperCase();
			}
			//percent
			else if(type === "%") {
				num *= 100;
				if(!this.inf_re.test(num)) {
					val = sign + (precision === undefined ? num.toString() : 
						num.toFixed(precision !== null ? precision : 1)) + "%";
				}
			}
			//unknown type
			else
				return null;
		}
		//else value is string
		else  {
			//string precision means maximum value width
			if(precision !== undefined && precision !== null) {
				val = val.toString();
				if(val.length > precision)
					val = val.substring(0, precision);
			}
		}
		
		val = val.toString();
		
		//if width is set and value is shorter
		if(width !== null && val.length < width) {
			//align left
			if(align === "<") {
				val = ljust(val, width, fill);
			}
			//align right
			else if(align === ">") {
				val = rjust(val, width, fill);
			}
			//align even
			else if(align === "^") {
				val = center(val, width, fill);
			}
			//padding after sign, valid for numeric types
			else if(align === "=" && type !== null && type !== "c") {
				tmp = val.charAt(0);
				
				//if sign was used
				if(tmp === "-" || tmp === "+" || tmp === " ") {
					val = val.substring(1);
					pre = tmp;
				}
				else
					pre = "";
				
				//if prefix was used
				if(prefix && (tmp = this.num_prefixes[type]) !== undefined) {
					val = val.substring(tmp.length);
					pre += tmp;
				}
				
				i = val.length + pre.length;
				for(; i<width; i++)
					val = fill + val;
				
				val = pre + val;
			}
		}
		
		return val !== null ? val.toString() : "";
	};
	
	
	/**
	 * Parse replacement token field name
	 * 
	 * @param string str Field name string
	 * @return mixed Corresponding value from this.repl
	 */
	Formatter.prototype.parseFieldName = function(str) {
		var i, tmp, val = null, start;
		
		//search first element
		i = str.indexOf("[");
		if(i === -1)
			i = str.length;
		
		//no element name
		if(i === 0)
			return null;
		
		tmp = str.substring(0, i);
		if(this.repl[tmp] === undefined)
			return null;
		
		val = this.repl[tmp];
		
		//search for additional elements with [] syntax
		for(; i < str.length; i++) {
			if(str.charAt(i) !== "[")
				return null;
			
			start = i + 1;
			i = str.indexOf("]", start);
			if(i === -1)
				return null;
			
			tmp = str.substring(start, i);
			
			//value must be either object or array with tmp property or index
			if(val[tmp] === undefined)
				return null;
			
			val = val[tmp];
		}
		
		return val;
	};
	
	
	//public interface
	return {
		"count": count,
		"blank": blank,
		"empty": empty,
		"startswith": startswith,
		"endswith": endswith,
		"contains": contains,
		"fill": fill,
		"replacesub": replacesub,
		"join": join,
		"lstrip": lstrip,
		"rstrip": rstrip,
		"strip": strip,
		"ljust": ljust,
		"rjust": rjust,
		"center": center,
		"encodehtml": encodehtml,
		"truncate": truncate,
		"ucwords": ucwords,
		"capitalize": capitalize,
		"format": format,
		"vformat": vformat
	};
	
})());
